﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using System.IO;
using System.Xml.Linq;
using System.Xml.XPath;
using System.Reflection;
using Ghost.Tasks.Wiki.XApi;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Xsl;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

namespace Ghost.Tasks.Wiki
{
    /// <summary>
    /// A task that generates google code wiki pages for types with XmlDocumentation
    /// </summary>
    public class WikiDoc : Task
    {


        public WikiDoc()
        {
            PagePrefix = "";
        }

        public string PagePrefix { get; set; }

        public string TypePageStylesheet { get; set; }

        public string AssemblyPageStylesheet { get; set; }

        public string NamespacePageStylesheet { get; set; }

        public string AssemblySidebarStylesheet { get; set; }

        public string NamespaceSidebarStylesheet { get; set; }

        public string TypeSidebarStylesheet { get; set; }

        [Required]
        public ITaskItem[] InputAssemblies { get; set; }

        public string OutputFolder { get; set; }

        [Output]
        public ITaskItem ProjectPage { get; private set; }

        [Output]
        public ITaskItem[] AssemblyPages { get; private set; }

        [Output]
        public ITaskItem[] NamespacePages { get; private set; }

        [Output]
        public ITaskItem[] TypePages { get; private set; }

        /// <summary>
        /// When overridden in a derived class, executes the task.
        /// </summary>
        /// <returns>
        /// true if the task successfully executed; otherwise, false.
        /// </returns>
        public override bool Execute()
        {
            XProject project = new XProject(Log);
            Log.LogMessageFromText("Starting WikiDoc transform", MessageImportance.Normal);

            //Set up our transformation scripts
            EmbeddedResourceResolver resolver = new EmbeddedResourceResolver(Assembly.GetExecutingAssembly(), "{0}.Wiki.{1}");
            XsltSettings settings = new XsltSettings();
            settings.EnableScript = true;

            XslTransformer assemblyPageTransformer = new XslTransformer("Ghost.Tasks.Wiki.AssemblyPage.xslt", AssemblyPageStylesheet, settings, resolver);
            XslTransformer namespacePageTransformer = new XslTransformer("Ghost.Tasks.Wiki.NamespacePage.xslt", NamespacePageStylesheet, settings, resolver);
            XslTransformer assemblySidebarTransformer = new XslTransformer("Ghost.Tasks.Wiki.AssemblySidebar.xslt", AssemblySidebarStylesheet, settings, resolver);
            XslTransformer namespaceSidebarTransformer = new XslTransformer("Ghost.Tasks.Wiki.NamespaceSidebar.xslt", NamespaceSidebarStylesheet, settings, resolver);
            XslTransformer typePageTransformer = new XslTransformer("Ghost.Tasks.Wiki.TypePage.xslt", TypePageStylesheet, settings, resolver);
            XslTransformer typePageSidebarTransformer = new XslTransformer("Ghost.Tasks.Wiki.TypeSidebar.xslt", TypeSidebarStylesheet, settings, resolver);


            try
            {                
                foreach (ITaskItem inputAssemblyItem in InputAssemblies)
                {
                    //Check for input assembly
                    if (!File.Exists(inputAssemblyItem.ItemSpec))
                    {
                        Log.LogWarning("Skipping. Could not find Assembly: {0}", inputAssemblyItem.ItemSpec);
                        continue;
                    }
                    //Load the assembly
                    Assembly assembly = Assembly.LoadFrom(inputAssemblyItem.ItemSpec);


                    string inputDir = Path.GetDirectoryName(inputAssemblyItem.ItemSpec);
                    string assemblyCommentsPath = Path.Combine(inputDir, Path.GetFileName(inputAssemblyItem.ItemSpec)).Replace(".dll", ".xml");

                    if (!File.Exists(assemblyCommentsPath))
                    {
                        Log.LogWarning("Skipping: Could not find XML Documentation file: {0}", assemblyCommentsPath);
                        continue;
                    }

                    //Default the output folder if needed
                    if (OutputFolder == null)
                    {
                        Log.LogMessageFromText("Defaulting OutputFolder using assembly path", MessageImportance.Normal);
                        OutputFolder = Path.GetDirectoryName(assemblyCommentsPath);
                    }

                    if (File.Exists(OutputFolder))
                        throw new Exception(string.Format("Destination Folder is invalid: {0}", OutputFolder));

                    try
                    {
                        if (!Directory.Exists(OutputFolder))
                        {
                            Log.LogMessageFromText("Creating Output Folder", MessageImportance.Normal);
                            Directory.CreateDirectory(OutputFolder);
                        }
                    }
                    catch (Exception x)
                    {
                        throw new Exception("Destination Folder does not exist and cannot be created", x);
                    }

                    //Build the assembly element
                    XAssembly xAssembly = BuildAssembly(project, assembly, assemblyCommentsPath);

                    project.AppendAssembly(xAssembly);

                    //Partition types by namespace
                    ILookup<string, Type> typesByNamespace = assembly.GetExportedTypes().ToLookup(t => t.Namespace);

                    List<XNameSpace> xNamespaces = new List<XNameSpace>(typesByNamespace.Count);

                    //Process each namespace
                    foreach (IGrouping<string, Type> typeGrouping in typesByNamespace)
                    {
                        XNameSpace xns = xAssembly.BuildNameSpace(typeGrouping.Key);
                        List<XType> xTypes = new List<XType>(typeGrouping.Count());
                        foreach (Type type in typeGrouping)
                        {
                            xTypes.Add(xAssembly.FindOrBuildXType(type, type.ToXmlCommentID()));
                        }

                        xns.@generic = xTypes.OfType<XGeneric>().ToArray();
                        xns.@class = xTypes.OfType<XClass>().Where(m => !(m is XGeneric)).ToArray();
                        xns.@interface = xTypes.OfType<XInterface>().Where(m => !(m is XClass)).ToArray();
                        xns.@enum = xTypes.OfType<XEnum>().ToArray();
                        xns.@struct = xTypes.OfType<XStruct>().Where(m => !(m is XClass)).ToArray();

                        xNamespaces.Add(xns);
                    }

                    //Fill in namespaces
                    xAssembly.@namespace = xNamespaces.ToArray();

                    xAssembly.xapiPath = Path.ChangeExtension(assemblyCommentsPath, "xapi");

                    //Create our own namespaces for the output
                    XmlSerializerNamespaces ns = new XmlSerializerNamespaces();

                    //Add an empty namespace and empty value
                    ns.Add("", "");

                    XmlSerializer serializer = new XmlSerializer(typeof(XAssembly));
                    using (TextWriter writer = new StreamWriter(xAssembly.xapiPath))
                    {
                        serializer.Serialize(writer, xAssembly, ns);
                    }
                }

                //Now resolve references and transform
                foreach (XAssembly xAssembly in project.Assemblies)
                {
                    project.ResolveCrefs(xAssembly.xapiPath);

                    assemblyPageTransformer.TransformToText(xAssembly.xapiPath, Path.ChangeExtension(xAssembly.wikiLink, "wiki"));
                    assemblySidebarTransformer.TransformToText(xAssembly.xapiPath, Path.ChangeExtension(xAssembly.sidebar, "wiki"));

                    foreach (XNameSpace outputNamespace in xAssembly.@namespace)
                    {
                        namespacePageTransformer.ArgumentList.Clear();
                        namespacePageTransformer.ArgumentList.AddParam("id", "", outputNamespace.id);
                        namespacePageTransformer.TransformToText(xAssembly.xapiPath, Path.Combine(OutputFolder, Path.ChangeExtension(outputNamespace.wikiLink, "wiki")));
                        namespaceSidebarTransformer.ArgumentList.Clear();
                        namespaceSidebarTransformer.ArgumentList.AddParam("id", "", outputNamespace.id);
                        namespaceSidebarTransformer.TransformToText(xAssembly.xapiPath, Path.Combine(OutputFolder, Path.ChangeExtension(outputNamespace.sidebar, "wiki")));
                    }

                    foreach (XType outputType in xAssembly.GetTypes())
                    {
                        string outputPath = Path.Combine(OutputFolder, Path.ChangeExtension(outputType.wikiLink, "wiki"));
                        typePageTransformer.ArgumentList.Clear();
                        typePageTransformer.ArgumentList.AddParam("id", "", outputType.id);
                        typePageTransformer.ArgumentList.AddParam("ns", "", outputType.ns);
                        typePageTransformer.TransformToText(xAssembly.xapiPath, outputPath);
                        typePageSidebarTransformer.ArgumentList.Clear();
                        typePageSidebarTransformer.ArgumentList.AddParam("id", "", outputType.id);
                        typePageSidebarTransformer.ArgumentList.AddParam("ns", "", outputType.ns);
                        typePageSidebarTransformer.TransformToText(xAssembly.xapiPath, Path.Combine(OutputFolder, Path.ChangeExtension(outputType.sidebar, "wiki")));
                    }
                }

                //Build up the task info:
                ProjectPage = project.ToTaskItem();
                AssemblyPages = project.Assemblies.Select(a => a.ToTaskItem()).ToArray();
                NamespacePages = project.Assemblies.SelectMany(a => a.@namespace).Select(n => n.ToTaskItem()).ToArray();
                TypePages = project.Members.OfType<XType>().Select(m => m.ToTaskItem()).ToArray();
            }
            catch (Exception e)
            {
                Log.LogErrorFromException(e);
                return false;
            }
            return true;
        }

 

        private XAssembly BuildAssembly(XProject project, Assembly inputAssembly, string assemblyCommentsPath)
        {
            XAssembly output = new XAssembly();
            output.project = project;
            output.inputAssembly = inputAssembly;
            output.log = project.log;
            output.id = string.Concat("A:", inputAssembly.GetName().Name);
            project.AppendAssembly(output);
            Log.LogMessageFromText(string.Format("Built Assembly : {0}", output.id), MessageImportance.Normal);
            output.wikiLink = output.id.ToWikiLink();
            output.sidebar = output.wikiLink.ToSidebar();
            output.caption = inputAssembly.GetName().Name + ".dll";
            output.fileName = Path.GetFileName(inputAssembly.Location);
            output.cultureInfo = inputAssembly.GetName().CultureInfo.ToString();
            output.version = inputAssembly.GetName().Version.ToString();
            output.BuildFileComments(output);

            output.BuildExtensionMethods();

            project.AddMember(output);

            //Load the input XML document
            XDocument source = XDocument.Load(assemblyCommentsPath, LoadOptions.None);

            //Build a dictionary based on the name/id of each member
            output.memberElements = source.Descendants("member").ToDictionary(memberElement => memberElement.Attribute("name").Value);

            return output;
        }

        private void BuildParametersElement(XElement parent, IEnumerable<ParameterInfo> parameterInfoItems)
        {
            if (parameterInfoItems.Any())
            {
                XElement element = new XElement("params");
                foreach (ParameterInfo pi in parameterInfoItems)
                {
                    BuildParameterInfoElement(element, pi);
                }
                parent.Add(element);
            }
        }

        private void BuildParameterInfoElement(XElement parent, ParameterInfo pi)
        {
            if (pi != null)
            {
                XElement element = new XElement("param");
                element.SetAttributeValue("isIn", pi.IsIn);
                element.SetAttributeValue("isLcid", pi.IsLcid);
                element.SetAttributeValue("isOptional", pi.IsOptional);
                element.SetAttributeValue("isOut", pi.IsOut);
                element.SetAttributeValue("isRetval", pi.IsRetval);
                if (pi.IsOptional)
                {
                    element.SetAttributeValue("defaultValue", pi.DefaultValue == null ? "null" : pi.DefaultValue.ToString());
                }
                element.SetAttributeValue("name", pi.Name);
                BuildAttributesElement(element, pi.GetCustomAttributes(false));
                //BuildMemberRef(element, pi.ParameterType);
                parent.Add(element);
            }
        }

        private void BuildAttributesElement(XElement parent, IEnumerable<object> customAttributes)
        {
            List<System.Attribute> attributes = new List<Attribute>(customAttributes.OfType<System.Attribute>());
            if (attributes.Any())
            {
                XElement element = new XElement("attributes");
                foreach (System.Attribute attr in attributes)
                {
                    element.SetAttributeValue(attr.TypeId.ToString(), attr.ToString());
                }
                parent.Add(element);
            }
        }

        private void BuildMethodElement(XElement parent, string elementName, MethodInfo methodInfo, int recursionLevel)
        {
            if (methodInfo != null && recursionLevel < 2)
            {
                XElement element = new XElement(elementName);
                FillMethodElement(element, methodInfo, recursionLevel);
                parent.Add(element);
            }
        }

        private void FillMethodElement(XElement element, MethodInfo methodInfo, int recursionLevel)
        {
            element.SetAttributeValue("callingConvention", methodInfo.CallingConvention.ToString());
            element.SetAttributeValue("name", methodInfo.Name);
            //element.SetAttributeValue("cref", GetRawName(methodInfo));
            element.SetAttributeValue("containsGenericParameters", methodInfo.ContainsGenericParameters);
            BuildMethodElement(element, "base", methodInfo.GetBaseDefinition(), ++recursionLevel);
            BuildParametersElement(element, methodInfo.GetParameters());
            element.SetAttributeValue("IsAbstract", methodInfo.IsAbstract);
            element.SetAttributeValue("IsConstructor", methodInfo.IsConstructor);
            element.SetAttributeValue("IsFinal", methodInfo.IsFinal);
            element.SetAttributeValue("IsGenericMethod", methodInfo.IsGenericMethod);
            element.SetAttributeValue("IsGenericMethodDefinition", methodInfo.IsGenericMethodDefinition);
            element.SetAttributeValue("IsStatic", methodInfo.IsStatic);
            element.SetAttributeValue("IsVirtual", methodInfo.IsVirtual);
            //BuildMemberRef(element, methodInfo.ReturnType);
        }

        //private string GetRawName(MethodInfo methodInfo)
        //{
        //    return string.Concat("M:", methodInfo.DeclaringType.FullName, methodInfo.Name);
        //}

        
    }
}
